package ru.ifmo.cppsc.grammar;

import ru.ifmo.cppsc.workers.FileWorker;

import java.util.ArrayList;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.File;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.transform.*;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Author: Olga Komaleva
 * Date: Jun 15, 2008
 */

public class Grammar {
    private ArrayList<Symbol> nonterminals;
    private ArrayList<Symbol> terminals;
    protected ArrayList<Production> productions;   // todo private
    protected Symbol START;
    public final Symbol EPSILON = new Symbol("epsilon") {
        public int getId() {
            assert false;
            return -1;
        }
    };

    public void load ( String fileName ) {
        BufferedReader f = null;                

        try {
            f = new BufferedReader(new FileReader(fileName));

            initTerminals(f);
            initNonTerminals(f);
            initProductions(f);

        } catch (Exception e) {
            try {
                e.printStackTrace();
                f.close();
            } catch (Exception ee) {
            }
        } finally {
            try {
                f.close();
            } catch (Exception ee) {
            }
        }
    }


    public boolean isTerminal(Symbol symbol) {
        return ((SymbolImpl)symbol).isTerminal;
    }

    public Symbol[] getNonTerminals() {
        return nonterminals.toArray(new Symbol[nonterminals.size()]);
    }

    public Symbol[] getTerminals() {
        return terminals.toArray(new Symbol[terminals.size()]);
    }

    public Symbol getSymbolById(int i) {
        assert(i >= 0) && (i < terminals.size() + nonterminals.size());

        if (i < nonterminals.size()) {
            return nonterminals.get(i);
        } else {
            return terminals.get(i - nonterminals.size());
        }
    }

    public int size() {
        return terminals.size() + nonterminals.size();
    }

    public Production[] getProductions() {
        return productions.toArray(new Production[productions.size()]);
    }

    public void print() {
        for (Symbol t : terminals) {
            System.out.print(t.getName() + '\t');
        }
        System.out.println("");

        for (Symbol nt : nonterminals) {
            System.out.print(nt.getName() + '\t');
        }
        System.out.println("");

        for (Production p : productions) {
            System.out.print(p.getLeft().getName() + "->");
            for (int i = 0; i < p.getRight().length; i++) {
                System.out.print(p.getRight()[i].getName());
            }
            System.out.println("");
        }

    }

    public Symbol addTerminal(String s) {
        Symbol t = new SymbolImpl(s, terminals.size(), true);
        terminals.add(t);
        return t;
    }

    protected Symbol addNonTerminal(String s) {
        Symbol nt = new SymbolImpl(s, nonterminals.size(), false);
        nonterminals.add(nt);
        return nt;
    }


    public Symbol getSymbolByName(String name) {
        if (name.equals("epsilon")) {
            return EPSILON;
        }

        for (Symbol terminal: terminals) {
            if (terminal.getName().equals(name)) return terminal;
        }

        for (Symbol nonterminal: nonterminals) {
            if (nonterminal.getName().equals(name)) return nonterminal;
        }

        assert false : name;
        return null;
    }

    private void initTerminals(BufferedReader f) throws IOException {
        ArrayList<String> termNames = FileWorker.read(f.readLine());
        terminals = new ArrayList<Symbol>();
        for (String tNames : termNames) {
            int i = terminals.size();
            terminals.add(new SymbolImpl(tNames, i, true));
        }
    }

    private void initNonTerminals(BufferedReader f) throws IOException {
        ArrayList<String> nonTermNames = FileWorker.read(f.readLine());
        nonterminals = new ArrayList<Symbol>();
        for (String ntNames : nonTermNames) {
            int i = nonterminals.size();
            nonterminals.add(new SymbolImpl(ntNames, i, false));
        }

        START = nonterminals.get(0);
    }

    private void initProductions(BufferedReader f) throws IOException {
        productions = new ArrayList<Production>();
        ArrayList<ArrayList<String>> prods = new ArrayList<ArrayList<String>>();
        String s;
        while ((s = f.readLine()) != null) {
            prods.add(FileWorker.read(s));
        }

        for (ArrayList<String> p : prods) {
            assert(p.size() > 0);

            Symbol left = getSymbolByName(p.get(0));
            assert(!isTerminal(left));
            ArrayList<Symbol> right = new ArrayList<Symbol>();

            for (int i = 1; i < p.size(); i++) {
                if (!p.get(i).equals("|")) {
                    right.add(getSymbolByName(p.get(i)));
                } else {
                    productions.add(new Production(left, right.toArray(new Symbol[right.size()]), ""));
                    right = new ArrayList<Symbol>();
                }
            }
            if (right.size() != 0) {
               productions.add(new Production(left, right.toArray(new Symbol[right.size()]), "")); 
            }

        }
    }

    public ArrayList<Symbol> getSymbols() {
        ArrayList<Symbol> res = new ArrayList<Symbol>();
        res.addAll(nonterminals);
        res.addAll(terminals);
        return res;
    }

    public Symbol getStart() {
        return START;
    }


    private class SymbolImpl extends Symbol {
        private int number;
        private final boolean isTerminal;

        public SymbolImpl(String name, int number, boolean isTerminal) {
            super(name);
            this.isTerminal = isTerminal;
            this.number = number;
        }

        public int getId() {
            if (isTerminal) {
                return number + nonterminals.size();
            } else {
                return number;
            }
        }
    }

    public boolean killNonTerminal(Symbol s) {
        int startProdNum = productions.size();
        assert !s.getName().equals("Assign");
        if (s == START) return false;
        if (isTerminal(s)) return false;
        ArrayList<Production> toRemoved = new ArrayList<Production>();

        for (Production p : getProductions()) {
            if (p.getLeft() == s) {
                for (Symbol s1 : p.getRight()) {
                    if (s1 == s) return false;
                }
                toRemoved.add(p);
            }
        }

        productions.removeAll(toRemoved);

        ArrayList<Production> toAdded1 = new ArrayList<Production>();
        ArrayList<Production> toRemoved1 = new ArrayList<Production>();

        for (Production p : getProductions()) {
            ArrayList<Integer> positions = new ArrayList<Integer>();
            for (int j = 0; j < p.getRight().length; j++) {
                if (p.getRight()[j] == s) {
                    positions.add(j);
                    p.print();
                }
            }

            if (positions.size() > 0) {
                assert(toRemoved.size() > 0) : s.getName() + " !==!";
                ArrayList<int[]> values = getAllValues(toRemoved.size(), positions.size());

                for (int[] value : values) {
                    Production p1 = p;
                    for (int j = value.length - 1; j >= 0; j--) {
                        p1 = p1.removeFromRight(positions.get(j), toRemoved.get(value[j]));
                        toAdded1.add(p1);
                        System.out.println(p1.toString());
                    }
                }
                toRemoved1.add(p);
            }
        }

        productions.removeAll(toRemoved1);
        productions.addAll(toAdded1);

        int endProdNum = productions.size();
        return true;
    }

    private ArrayList<int[]> getAllValues(int m, int k) {
        ArrayList<int[]> res = new ArrayList<int[]>();

        int[] value = new int[k]; // TODO 0 --- default value

        int i = 0;
        while (true) {
            res.add(value);

            int[] value1 = new int[k];
            System.arraycopy(value, 0, value1, 0, value.length);
            value = value1;

            value[k - 1] = (value[k - 1] + 1)%m;

            int ind = k - 2;
            while ((ind >= 0) && (value[ind + 1] == 0)) {
                value[ind] = (value[ind] + 1)%m;
                ind--;
            }
            if ((ind == -1) && (value[0] == 0)) {
                return res;
            }
            i++;
            assert i < 1000;
        }
    }

    protected void resetGrammar(ArrayList<Symbol> terms, ArrayList<Symbol> nonterms, ArrayList<Production> prods) {
        terminals = terms;
        nonterminals = nonterms;
        productions = prods;

        for (int i = 0; i < nonterminals.size(); i++) {
            ((SymbolImpl)nonterminals.get(i)).number = i;
        }
        for (int i = 0; i < terminals.size(); i++) {
            ((SymbolImpl)terminals.get(i)).number = i;
        }
    }

    public void saveAsXML( String fileName ) {
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();                      
            factory.setValidating(true);
            DocumentBuilder builder = factory.newDocumentBuilder();

            Document doc = builder.newDocument();
            Element root = doc.createElement("grammar");
            doc.appendChild( root );

            Node termNodes = doc.createElement("terminals");
            root.appendChild(termNodes);
            for ( Symbol terminal : terminals ) {
                Node termNode = doc.createElement ("terminal");
                termNodes.appendChild(termNode);
                Node contents = doc.createTextNode( terminal.getName());
                termNode.appendChild(contents);
            }

            Node nontermNodes = doc.createElement("nonterminals");
            root.appendChild(nontermNodes);
            for ( Symbol nonterminal : nonterminals ) {
                Node nontermNode = doc.createElement ("nonterminal");
                nontermNodes.appendChild( nontermNode );
                Node contents = doc.createTextNode( nonterminal.getName());
                nontermNode.appendChild(contents);
            }


            Node prodNodes = doc.createElement("productions");
            root.appendChild(prodNodes);
            for ( Production production : productions ) {
                Node prodNode = doc.createElement("production");
                prodNodes.appendChild( prodNode );

                Node leftNode = doc.createElement("left");
                prodNode.appendChild(leftNode);
                Node leftText = doc.createTextNode(production.getLeft().getName());
                leftNode.appendChild(leftText);

                Node rightNode = doc.createElement ("right");
                prodNode.appendChild(rightNode);
                String rStr = "";
                for ( Symbol rSym : production.getRight()) {
                    rStr += rSym + " ";
                }
                rStr = rStr.substring ( 0, rStr.length() - 1 );
                Node contents = doc.createTextNode(rStr);
                rightNode.appendChild ( contents );

                Node codeNode = doc.createElement("code");
                prodNode.appendChild(codeNode);
                Node codeContents = doc.createTextNode("\n\n");
                codeNode.appendChild(codeContents);
            }

            Source source = new DOMSource(doc);
            File file = new File( fileName );
            Result result = new StreamResult( file );
            Transformer xformer = TransformerFactory.newInstance().newTransformer();
            xformer.setOutputProperty(OutputKeys.INDENT, "yes");
            xformer.transform ( source, result );
        } catch ( TransformerConfigurationException e ) {
        } catch ( TransformerException e ) {
        } catch ( ParserConfigurationException e ) {
        }
    }

    public void loadAsXML ( String fileName ) {
        terminals = new ArrayList<Symbol>();
        nonterminals = new ArrayList<Symbol>();
        productions = new ArrayList<Production>();

        Document doc = null;
        try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();                      
            factory.setValidating(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            doc = builder.parse ( new File ( fileName ));
        } catch ( ParserConfigurationException e ) {
        } catch ( SAXException e ) {
        } catch ( IOException e ) {
        }

        Element root = doc.getDocumentElement();
        NodeList termList = root.getElementsByTagName("terminal");
        for ( int i = 0; i < termList.getLength(); ++i ) {
            Node terminal = termList.item(i);
            terminals.add(new SymbolImpl ( terminal.getTextContent(), terminals.size(), true ));
        }

        NodeList nontermList = root.getElementsByTagName ("nonterminal");
        for ( int i = 0; i < nontermList.getLength(); ++i ) {
            Node nonterm = nontermList.item (i);
            nonterminals.add( new SymbolImpl ( nonterm.getTextContent(), nonterminals.size(), false ));
        }

        NodeList prodList = root.getElementsByTagName("production");
        for ( int i = 0; i < prodList.getLength(); ++i ) {
            Element node = (Element)prodList.item(i);

            NodeList leftList = node.getElementsByTagName("left");
            Symbol leftSym = getSymbolByName ( leftList.item(0).getTextContent());

            NodeList rightList = node.getElementsByTagName("right");
            ArrayList<Symbol> rightSym = new ArrayList<Symbol>();

            String[] ss = rightList.item(0).getTextContent().split("[ ]");
            for ( String symName : ss ) {
                rightSym.add(getSymbolByName(symName));    
            }

            NodeList codeList = node.getElementsByTagName("code");
            String code = codeList.item(0).getTextContent();

            productions.add(new Production(leftSym, rightSym.toArray(new Symbol[rightSym.size()]), code));
        }
        START = nonterminals.get(0);
    }
}
